See console in devtool

<script>
  globalThis.NativeFormData = FormData
  globalThis.NativeFile = File
  window.FormData = undefined
  // Want to simulate the kind of error we get in
  // old safari & ie when constructing a file
  window.File = new Proxy(NativeFile, {
    construct (target, args) {
      throw new Error()
    }
  })
</script>
<script src="../formdata.min.js"></script>
<!-- <script src="../FormData.js"></script> -->
<script type="module">
  import {formDataToBlob} from '../formdata-to-blob.js'

  const Polyfill = globalThis.FormData

  function catchErr(fn) {
    try { fn() } catch (err) { return err }
  }

  for (const FormData of [NativeFormData, Polyfill]) {
    function createFormData (...args) {
      const fd = new FormData()
      for (const arg of args) {
        fd.append(...arg)
      }
      return fd
    }

    function createForm (str) {
      var form = document.createElement('form')
      form.innerHTML = str
      return new FormData(form)
    }

    const impl = FormData === NativeFormData ? 'native' : 'polyfill'

    {
      const fd = new FormData()
      const err = catchErr(() => fd.append('key'))
      console.assert(err instanceof TypeError, 'should throw on few arguments')
    }

    // #120 escapes keys when encoding FormData
    if (impl === 'polyfill') {
      for (let [key, expected] of [['key\n', 'key%0D%0A'], ['key\r', 'key%0D%0A'], ['key"', 'key%22']]) {
        const origFd = createFormData([key, 'value'])
        const fd = await new Response(origFd._blob ? origFd._blob() : origFd).formData()
        console.assert(fd.has(expected) === true)

        const blob = formDataToBlob(origFd)
        const fd2 = await new Response(blob).formData()
        console.assert(fd2.has(expected) === true)
      }
    }

    // #120 normalizes linebreaks
    // {
    //   var fd = new FormData()
    //   fd.set('a', '\n')
    //   fd.set('b', '\r')
    //   fd.set('c', '\n\r')

    //   console.assert(fd.get('a') === '\n')
    //   console.assert(fd.get('b') === '\r')
    //   console.assert(fd.get('c') === '\n\r')

    //   new Response(formDataToBlob(fd)).formData().then(fd2 => {
    //     console.assert(fd2.get('a') === '\r\n')
    //     console.assert(fd2.get('b') === '\r\n')
    //     console.assert(fd2.get('c') === '\r\n\r\n')
    //   })
    // }

    // #78
    {
      const fd = createForm(`<input name=foo type=file>`)
      console.assert(fd.has('foo') === true)
      console.assert(fd.get('foo').type === 'application/octet-stream')
    }

    {
      const fd = createFormData(['key', 'value1'])
      console.assert(fd.get('key') === 'value1')
    }

    {
      const fd = createFormData(['key', 'value2'], ['key', 'value1'])
      console.assert(fd.get('key') === 'value2')
    }

    {
      const fd = createFormData(['key', undefined])
      console.assert(fd.get('key') === 'undefined')
    }

    {
      const fd = createFormData(['key', undefined], ['key', 'value1'])
      console.assert(fd.get('key') === 'undefined')
    }

    {
      const fd = createFormData(['key', null])
      console.assert(fd.get('key') === 'null')
    }

    {
      const fd = createFormData(['key', null], ['key', 'value1'])
      console.assert(fd.get('key') === 'null')
    }

    {
      const fd = new FormData(document.createElement('form'))
      fd.append('key', 'value1')
      console.assert(fd.get('key') === 'value1')
    }

    {
      const fd = new FormData(document.createElement('form'))
      fd.append('key', 'value2')
      fd.append('key', 'value1')
      console.assert(fd.get('key') === 'value2')
    }

    {
      const fd = new FormData(document.createElement('form'))
      fd.append('key', undefined)
      console.assert(fd.get('key') === 'undefined')
    }

    {
      const fd = new FormData(document.createElement('form'))
      fd.append('key', undefined)
      fd.append('key', 'value1')
      console.assert(fd.get('key') === 'undefined')
    }

    {
      const fd = new FormData(document.createElement('form'))
      fd.append('key', null)
      console.assert(fd.get('key') === 'null')
    }

    {
      const fd = new FormData(document.createElement('form'))
      fd.append('key', null)
      fd.append('key', 'value1')
      console.assert(fd.get('key') === 'null')
    }

    {
      const fd = createFormData(['key', new Blob(), 'blank.txt'])
      const file = fd.get('key')
      console.assert(file.name === 'blank.txt')
      console.assert(file.type === '')
      console.assert(file.size === 0)
      if (impl === 'polyfill') {
        // Chrome dose wrong...
        console.assert(fd.get('key') === fd.get('key'), 'File should be same instances')
      }
    }

    console.info('testing has() on ' + impl)

    {
      const fd = new FormData()
      const err = catchErr(() => fd.has())
      console.assert(err instanceof TypeError)
    }

    {
      const fd = new FormData()
      fd.append('n1', 'value')
      console.assert(fd.has('n1') === true)
      console.assert(fd.has('n2') === false)
      fd.append('n2', 'value')
      console.assert(fd.has('n1') === true)
      console.assert(fd.has('n2') === true)
      fd.append('n3', new Blob(['content']))
      console.assert(fd.has('n3') === true)
    }

    console.info('testing entries() on ' + impl)

    {
      const fd = createFormData(
        ['keyA', 'val1'],
        ['keyA', 'val2'],
        ['keyB', 'val3'],
        ['keyA', 'val4']
      )

      const actualKeys = [...fd.keys()] + ''
      const expectedKeys = ['keyA', 'keyA', 'keyB', 'keyA'] + ''
      console.assert(actualKeys === expectedKeys, 'should return the keys as they are appended')

      const actualValues = [...fd.values()] + ''
      const expectedValues = ['val1', 'val2', 'val3', 'val4'] + ''
      console.assert(actualValues === expectedValues, 'should return values as they are appended')

      const actualEntries = [...fd] + ''
      const expectedEntries = [
        ['keyA', 'val1'],
        ['keyA', 'val2'],
        ['keyB', 'val3'],
        ['keyA', 'val4']
      ] + ''
      console.assert(actualEntries === expectedEntries, 'should return the entres as they are appended')
    }

    console.info('testing set() on ' + impl)

    {
      const fd = new FormData()
      const err = catchErr(() => fd.set())
      console.assert(err instanceof TypeError, 'should throw on few arguments')
    }

    {
      const fd = createFormData(
        ['keyA', 'val1'],
        ['keyA', 'val2'],
        ['keyB', 'val3'],
        ['keyA', 'val4']
      )
      fd.set('keyA', 'val3')
      const actual = String([...fd])
      const expected = String([['keyA', 'val3'], ['keyB', 'val3']])
      const msg = 'Overwrite first matching key'
      console.assert(actual === expected, msg)
    }

    {
      const fd = createFormData(['keyB', 'val3'])
      fd.set('keyA', 'val3')
      const actual = String([...fd])
      const expected = String([['keyB', 'val3'], ['keyA', 'val3']])
      const msg = 'appends value when no matching'
      console.assert(actual === expected, msg)
    }

    console.info('testing delete() on ' + impl)

    {
      const fd = new FormData()
      const err = catchErr(() => fd.delete())
      console.assert(err instanceof TypeError, 'should throw on few arguments')
    }

    {
      var fd = new FormData()
      fd.append('name', 'value')
      console.assert(fd.has('name') === true, 'name should have been appended')
      fd.delete('name')
      console.assert(fd.has('name') === false, 'name should have been deleted')

      fd.append('name', new Blob(['content']))
      console.assert(fd.has('name') === true, 'it should have a name field')
      fd.delete('name')
      console.assert(fd.has('name') === false, 'name should have been deleted')

      fd.append('n1', 'v1')
      fd.append('n2', 'v2')
      fd.append('n1', 'v3')
      fd.delete('n1')
      console.assert(fd.has('n1') === false, 'delete should remove all keys with n1')
      console.assert(String([...fd]) === String([['n2', 'v2']]))
    }

    {
      // #43
      // Old ie don't have Symbol.toStringTag and the polyfill was
      // therefore not able to change the
      // `Object.prototype.toString.call` to return correct type of the polyfilled
      // File constructor
      const fd = createFormData(['key', new NativeFile([], 'doc.txt')])
      const mockFile = fd.get('key')
      console.assert(mockFile.name === 'doc.txt', 'Should return correct filename with File')
    }

    {
      const fd = createFormData(['key', new Blob(), 'doc.txt'])
      const mockFile = fd.get('key')
      console.assert('doc.txt', mockFile.name, 'Should return correct filename with Blob filename')
    }

    {
      const fd = createFormData(['key', new Blob()])
      const mockFile = fd.get('key')
      console.assert('blob' === mockFile.name, 'Default name should be Blob')
    }

    console.info('testing linebreaks() on ' + impl)
    {
      const form = document.createElement('form')
      const textarea = document.createElement('textarea')
      textarea.name = 'key'
      form.append(textarea)

      // Native FormData normalizes linefeeds in textareas to CRLF
      // In order to be consistent the polyfill should do the same
      {
        textarea.value = '\n'
        const fd = new FormData(form)
        const value = fd.get('key')
        console.assert('\r\n' === value, 'Should convert LF to CRLF for textareas')
      }

      {
        textarea.value = '\r'
        const fd = new FormData(form)
        const value = fd.get('key')
        console.assert('\r\n' === value, 'Should convert CR to CRLF for textareas')
      }

      {
        textarea.value = 'a\n\ra\r\na\n\r\n\r\n\r\n\na\r\r'
        const fd = new FormData(form)
        const value = fd.get('key')
        console.assert(
          'a\r\n\r\na\r\na\r\n\r\n\r\n\r\n\r\na\r\n\r\n' === value,
          'Should normalize mixed linefeeds to CRLF for textareas'
        )
      }

      {
        const fd = createFormData(['key', '\n'])
        const value = fd.get('key')
        console.assert('\n' === value, 'Should not convert LF to CRLF when provided by append')
      }

      {
        const fd = createFormData(['key', '\r'])
        const value = fd.get('key')
        console.assert('\r' === value, 'Should not convert CR to CRLF when provided by append')
      }
    }

    console.info('testing disabled() on ' + impl)

    {
      const fd = createForm(`<input disabled name=foo value=bar>`)
      console.assert(fd.has('foo') === false, 'Should not include disabled inputs')
    }

    // #110
    {
      const fd = createForm(`<fieldset disabled><input name=foo value=bar></fieldset>`)
      console.assert(fd.has('foo') === false, 'Should not include disabled fieldset inputs')
    }

    // #56
    {
      const fd = createForm(`
        <select multiple name="example">
          <option selected disabled value="foo">Please choose one</option>
        </select>
      `)

      console.assert(
        fd.has('example') === false,
        'Select elements where the option is both selected and disabled should not be included'
      )
    }

    // #45
    {
      const fd = createForm(`
        <select multiple name="example">
          <option selected value="volvo">Volvo</option>
          <option selected value="saab">Saab</option>
          <option value="mercedes">Mercedes</option>
          <option value="audi">Audi</option>
        </select>
      `)

      console.assert(
        String([...fd]) === String([['example', 'volvo'], ['example', 'saab']]),
        'Should return selected items'
      )
    }

    // #62
    {
      const fd = createForm(`
        <input type="text" name="username" value="bob">
        <input type="submit" value="rename" name="action">
        <input type="submit" value="find_n_delete" name="action">
      `)

      console.assert(
        String([...fd]) === String([['username', 'bob']]),
        'Should not add buttons to FormData'
      )
    }

    // chrome dose wrong...
    if (impl === 'polyfill') {
      var fd = new FormData()
      fd.set('a', 'a\na')
      fd.set('b', 'b\rb')
      fd.set('c', 'c\n\rc')

      console.assert(fd.get('a') === 'a\na')
      console.assert(fd.get('b') === 'b\rb')
      console.assert(fd.get('c') === 'c\n\rc')

      await new Response(fd._blob()).text().then(str => {
        console.assert(str.includes('a\r\na'))
        console.assert(str.includes('b\r\nb'))
        console.assert(str.includes('c\r\n\r\nc'))
      })
    }

    {
      var fd = new FormData()
      fd.set('a', 'a\na')
      fd.set('b', 'b\rb')
      fd.set('c', 'c\n\rc')

      console.assert(fd.get('a') === 'a\na')
      console.assert(fd.get('b') === 'b\rb')
      console.assert(fd.get('c') === 'c\n\rc')

      await new Response(formDataToBlob(fd)).text().then(str => {
        console.assert(str.includes('a\r\na'))
        console.assert(str.includes('b\r\nb'))
        console.assert(str.includes('c\r\n\r\nc'))
      })
    }

    // #86
    {
      const formData = createFormData(['key', 'value1'])
      const xhr = new XMLHttpRequest()
      xhr.open('POST', 'https://httpbin.org/post')
      xhr.setRequestHeader('Content-Type', 'text/plain')
      xhr.responseType = 'json'
      xhr.onload = () => {
        console.assert(
          xhr.response.headers['Content-Type'] === 'text/plain',
          'Shouldn not add Content-Type header if already present'
        )
      }
      xhr.send(formData)
    }

    const kTestChars = "ABC~‾¥≈¤･・•∙·☼★星🌟星★☼·∙•・･¤≈¥‾~XYZ"

    // formDataPostFileUploadTest - verifies multipart upload structure and
    // numeric character reference replacement for filenames, field names,
    // and field values using FormData and fetch().
    //
    // Uses /fetch/api/resources/echo-content.py to echo the upload
    // POST (unlike in send-file-form-helper.js, here we expect all
    // multipart/form-data request bodies to be UTF-8, so we don't need to
    // escape controls and non-ASCII bytes).
    //
    // Fields in the parameter object:
    //
    // - fileNameSource: purely explanatory and gives a clue about which
    //   character encoding is the source for the non-7-bit-ASCII parts of
    //   the fileBaseName, or Unicode if no smaller-than-Unicode source
    //   contains all the characters. Used in the test name.
    // - fileBaseName: the not-necessarily-just-7-bit-ASCII file basename
    //   used for the constructed test file. Used in the test name.
    async function formDataPostFileUploadTest(fileBaseName, asBlob = false) {
      if (impl !== 'polyfill') return
      const formData = new FormData();
      const file = new File([kTestChars], fileBaseName, { type: "text/plain" });

      // Used to verify that the browser agrees with the test about
      // field value replacement and encoding independently of file system
      // idiosyncracies.
      formData.append("filename", fileBaseName);

      // Same, but with name and value reversed to ensure field names
      // get the same treatment.
      formData.append(fileBaseName, "filename");

      formData.append("file", file, fileBaseName);

      const formDataText = await (asBlob ? formDataToBlob(formData).text() : new Response(formData._blob()).text());
      const formDataLines = formDataText.split("\r\n");
      if (formDataLines.length && !formDataLines[formDataLines.length - 1]) {
        formDataLines.length--
      }
      console.assert(formDataLines.length > 2, `${fileBaseName}: multipart form data must have at least 3 lines: ${
        JSON.stringify(formDataText)
      }`)

      const boundary = formDataLines[0];
      console.assert(
        formDataLines[formDataLines.length - 1] === boundary + "--",
        `${fileBaseName}: multipart form data must end with ${boundary}--: ${
          JSON.stringify(formDataText)
        }`,
      );

      const asValue = fileBaseName.replace(/\r\n?|\n/g, "\r\n");
      const asName = asValue.replace(/[\r\n"]/g, encodeURIComponent);
      const asFilename = fileBaseName.replace(/[\r\n"]/g, encodeURIComponent);
      const expectedText = [
        boundary,
        'Content-Disposition: form-data; name="filename"',
        "",
        asValue,
        boundary,
        `Content-Disposition: form-data; name="${asName}"`,
        "",
        "filename",
        boundary,
        `Content-Disposition: form-data; name="file"; ` +
        `filename="${asFilename}"`,
        "Content-Type: text/plain",
        "",
        kTestChars,
        boundary + "--",
      ].join("\r\n")

      console.assert(
        formDataText.startsWith(expectedText),
        `Unexpected multipart-shaped form data received:\n${formDataText}\nExpected:\n${expectedText}`,
      )

      if (!asBlob) return formDataPostFileUploadTest(fileBaseName, true)
    }

    console.log('Testing many names')
    await formDataPostFileUploadTest("file-for-upload-in-form.txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-\uF7F0\uF793\uF783\uF7A0.txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-â˜ºðŸ˜‚.txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-★星★.txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-☺😂.txt")
    await formDataPostFileUploadTest(`file-for-upload-in-form-${kTestChars}.txt`)

    await formDataPostFileUploadTest("file-for-upload-in-form.txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-NUL-[\0].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-BS-[\b].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-VT-[\v].txt")

    // These have characters that undergo processing in name=,
    // filename=, and/or value; formDataPostFileUploadTest postprocesses
    // expectedEncodedBaseName for these internally.
    await formDataPostFileUploadTest("file-for-upload-in-form-LF-[\n].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-LF-CR-[\n\r].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-CR-[\r].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-CR-LF-[\r\n].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-HT-[\t].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-FF-[\f].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-DEL-[\x7F].txt")

    // The rest should be passed through unmodified:
    await formDataPostFileUploadTest("file-for-upload-in-form-ESC-[\x1B].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-SPACE-[ ].txt")


    // These have characters that undergo processing in name=,
    // filename=, and/or value; formDataPostFileUploadTest postprocesses
    // expectedEncodedBaseName for these internally.
    await formDataPostFileUploadTest("file-for-upload-in-form-QUOTATION-MARK-[\x22].txt")
    await formDataPostFileUploadTest('"file-for-upload-in-form-double-quoted.txt"')
    await formDataPostFileUploadTest("file-for-upload-in-form-REVERSE-SOLIDUS-[\\].txt")

    // The rest should be passed through unmodified:
    await formDataPostFileUploadTest("file-for-upload-in-form-EXCLAMATION-MARK-[!].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-DOLLAR-SIGN-[$].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-PERCENT-SIGN-[%].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-AMPERSAND-[&].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-APOSTROPHE-['].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-LEFT-PARENTHESIS-[(].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-RIGHT-PARENTHESIS-[)].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-ASTERISK-[*].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-PLUS-SIGN-[+].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-COMMA-[,].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-FULL-STOP-[.].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-SOLIDUS-[/].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-COLON-[:].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-SEMICOLON-[;].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-EQUALS-SIGN-[=].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-QUESTION-MARK-[?].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-CIRCUMFLEX-ACCENT-[^].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-LEFT-SQUARE-BRACKET-[[].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-RIGHT-SQUARE-BRACKET-[]].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-LEFT-CURLY-BRACKET-[{].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-VERTICAL-LINE-[|].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-RIGHT-CURLY-BRACKET-[}].txt")
    await formDataPostFileUploadTest("file-for-upload-in-form-TILDE-[~].txt")
    await formDataPostFileUploadTest("'file-for-upload-in-form-single-quoted.txt'")
  }
</script>
